# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu, jaewon

import concurrent.futures
import datetime
import functools
import logging
import json
import multiprocessing
import os
import shutil
import urllib.request

import tabulate
from absl import app, flags

from coin.base.datetime_util import to_datetime
from coin.base.param_util import to_list
from coin.strategy.tool.monitor.log_parser_runner import (check_utc, run_config_v1, run_config_v2)
from coin.strategy.tool.monitor import config_v1_1, config_v1_2, config_v2
from coin.strategy.tool.monitor.plot import (setup_plt,
                                             get_plotter,
                                             refine_balance,
                                             refine_timeseries)


@functools.lru_cache()
def get_bitmex_xbtusd_volume_24h():
  try:
    xbtusd_info_url = 'https://www.bitmex.com/api/v1/instrument?symbol=XBTUSD&count=1'
    content = urllib.request.urlopen(xbtusd_info_url, timeout=5).read()
    parsed = json.loads(content)
    volume_24h = int(parsed[0]['volume24h'])
    return volume_24h
  except Exception:
    return None


def get_logt_names(results):
  logt_names = []
  for result in results:
    if 'last_log' not in result:
      continue
    retired = result['name'].startswith('RETIRED')
    if not retired:
      logt_names.append((result['last_log'], result['name']))
  return logt_names


def report(mode, results, sort_by_name=True):
  if mode == 'monitor':
    headers = [
        'owner',
        'name',
        'pnl_mark',
        'pnl_4h',
        'pnl_24h',
        'turnover_mark',
        'turnover_4h',
        'turnover_24h',
        'position_2',
        'fill_2',
        'mark_time',
        'last_log',
        'processing_time'
    ]
  elif mode == 'operation':
    headers = [
        'owner',
        'name',
        'account_value',
        'mark_value',
        'fill_1',
        'fill_2',
        'balance_1',
        'balance_2'
    ]
  elif mode == 'report':
    headers = ['owner', 'name', 'pnl_mark', 'turnover_mark', 'mark_time']
  else:
    raise ValueError('Invalid mode: %s' % mode)

  data = []
  pnl_mark_sum = 0
  position_sum = 0
  turnover_mark_sum = 0
  turnover_24h_sum = 0
  xbtusd_turnover_24h_sum = 0

  if sort_by_name:
    results = sorted(results, key=lambda v: v['name'])

  for result in results:
    retired = result['name'].startswith('RETIRED')
    entry = []
    for header in headers:
      if retired and header.startswith('position_'):
        entry.append(None)
      else:
        entry.append(result[header])

    data.append(entry)
    pnl_mark_sum += (result['pnl_mark'] or 0)
    if not retired:
      position_sum += (result['position_1'] or 0)
      position_sum += (result['position_2'] or 0)
    turnover_mark_sum += (result['turnover_mark'] or 0)
    turnover_24h_sum += (result['turnover_24h'] or 0)

    if (result['quote'] == 'BTC'
        and ('xbtusd' in result['name'] or 'bitmex-xbtusd' in result['name'])):
      xbtusd_turnover_24h_sum += (result['turnover_24h'] or 0)

  print(tabulate.tabulate(data, headers=headers, floatfmt='.3f'))
  if 'pnl_mark' in headers:
    print(' * Total pnl_mark:      {:18,.03f}'.format(pnl_mark_sum))
  if 'position_1' in headers:
    print(' * Total position:      {:18,.03f}'.format(position_sum))
  if 'turnover_mark' in headers and turnover_mark_sum != 0:
    print(' * Total turnover_mark: {:18,.03f}'.format(turnover_mark_sum))
  if 'turnover_24h' in headers and turnover_mark_sum != 0:
    print(' * Total turnover_24h:  {:18,.03f}'.format(turnover_24h_sum))
  if 'turnover_24h' in headers and xbtusd_turnover_24h_sum != 0:
    xbtusd_market_volume = get_bitmex_xbtusd_volume_24h()
    if xbtusd_market_volume is None:
      print(' * XBTUSD turnover_24h: {0:18,.03f}'.format(xbtusd_turnover_24h_sum))
    else:
      market_share_pct = 100. * xbtusd_turnover_24h_sum / xbtusd_market_volume
      print(' * XBTUSD turnover_24h: {0:18,.03f}  (market share: {1:,.02f}%)'.format(
          xbtusd_turnover_24h_sum, market_share_pct))


def plot_balance_pos(results, begin_time, end_time, mark_time, filename):
  assert begin_time < end_time
  with get_plotter(filename, begin_time, end_time, plot_secondary=True) as plots:
    for result in results:
      plots[0](*refine_balance(result, begin_time, end_time, mark_time))
      plots[1](*refine_timeseries(result, begin_time, end_time, 'position_dump'))


def _aggregate_by_key(iterable, key):
  res = {}
  for elem in iterable:
    k = elem[key]
    if k not in res:
      res[k] = []
    res[k].append(elem)
  return res


def _get_log_parser_input(strategy_group):
  v1_input_1 = config_v1_1.STRATEGIES
  v1_input_2 = config_v1_2.STRATEGIES
  v2_input = config_v2.LOG_FILES
  if strategy_group is None:
    v1_input = v1_input_1 + v1_input_2
  elif strategy_group == 1:
    v1_input = v1_input_1
    v2_input = []
  elif strategy_group == 2:
    v1_input = v1_input_2
  else:
    raise ValueError('Unknown strategy_group: %s' % strategy_group)
  return v1_input, v2_input


def encode_names(names, nabbr=60):
  nameslong = ",".join(names)
  if len(nameslong) > nabbr:
    nameslong = "%s..." % nameslong[:nabbr]
  return "%s(#:%s)" % (nameslong, len(names))


def process_log(strategy_group, current_time):
  print('Time: %s' % current_time.strftime('%Y%m%d-%H%M%S'))

  v1_input, v2_input = _get_log_parser_input(strategy_group)

  futures = []
  results = []
  max_workers = max(1, int(multiprocessing.cpu_count() * 0.8))  # better not die
  with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) \
       as executor:
    for log_path in v2_input:
      if not isinstance(log_path, str):
        continue
      fut = executor.submit(run_config_v2, log_path, current_time)
      futures.append((log_path, fut))

    for config in v1_input:
      if not isinstance(config, dict):
        continue
      fut = executor.submit(run_config_v1, config, current_time)
      futures.append((config['log_path'], fut))

    for log_path, f in futures:
      try:
        results += to_list(f.result(timeout=300))
      except concurrent.futures.TimeoutError:
        print('Timeout: %s' % log_path)
        continue
  return results


def dump_result(mode, results, today_dt, do_plot, plot_dir, plot_tmp):
  results_by_quote = _aggregate_by_key(results, 'quote')
  results_by_group = _aggregate_by_key(results, 'group')

  logt_names = []
  for quote, res_list in results_by_quote.items():
    logt_names += get_logt_names(res_list)

  if mode == 'monitor':
    print("Top 5 late logs & strategies:")
    top5lates = sorted(logt_names, reverse=True)[:5]
    top5times, top5names = zip(*top5lates)
    print(tabulate.tabulate([top5times], headers=top5names))
    print("\n")

  for quote, res_list in results_by_quote.items():
    print('Quote: %s' % quote)
    report(mode, res_list)
    print()

  if do_plot:
    plot_dir = os.path.normpath(os.path.expanduser(plot_dir))
    tmp_dir = os.path.normpath(os.path.expanduser(plot_tmp))
    os.makedirs(plot_dir, exist_ok=True)
    os.makedirs(tmp_dir, exist_ok=True)

    begin_time = today_dt + datetime.timedelta(hours=-2)
    end_time = today_dt + datetime.timedelta(days=1)
    mark_time = today_dt
    for group, res_list in results_by_group.items():
      filename = os.path.join(tmp_dir, '%s.png' % group)
      plot_balance_pos(res_list, begin_time, end_time, mark_time, filename)

    plot_date_dir = os.path.join(plot_dir, today_dt.strftime('%Y%m%d'))
    if os.path.exists(plot_date_dir):
      shutil.rmtree(plot_date_dir, ignore_errors=True)
    shutil.move(tmp_dir, plot_date_dir)


def main(argv):
  FLAGS = flags.FLAGS

  assert check_utc(), 'Invalid timezone'
  assert argv[1] in ['monitor', 'operation', 'report']
  mode = argv[1]
  strategy_group = FLAGS.strategy_group

  current_time = datetime.datetime.now()
  today_dt = to_datetime(current_time.date())
  if current_time - today_dt == datetime.timedelta():
    current_time -= datetime.timedelta(microseconds=1)
    today_dt = to_datetime(current_time.date())

  if len(argv) > 2:
    try:
      current_time = datetime.datetime.strptime(argv[2], '%Y%m%d-%H%M%S')
    except ValueError:
      current_time = datetime.datetime.strptime(argv[2], '%Y%m%d')
    today_dt = current_time

  results = process_log(strategy_group, current_time)
  dump_result(mode, results, today_dt, FLAGS.plot, FLAGS.plot_dir, FLAGS.plot_tmp)


if __name__ == '__main__':
  logging.basicConfig(level='DEBUG', format='%(levelname)8s %(asctime)s %(name)s] %(message)s')

  flags.DEFINE_integer('strategy_group', None, 'Strategy group to parse')
  flags.DEFINE_bool('plot', False, 'If true, plot balance and position')
  flags.DEFINE_string('plot_dir', 'plot_out', 'Plot directory')
  flags.DEFINE_string('plot_tmp', '.plot_tmp', 'Plot temporary directory')
  app.run(main)
